﻿
using lmsft;
using lmsft.Media.Codec.Audio;
using lmsft.RTP;
using lmsft.SDP;
using lmsft.SIP.Stack;
using lmsft_sip.codec;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;

namespace lmsft_sip {
    public class rtp_media {

        private static Dictionary<int, AudioCodec> _audio_codec;
        public static Dictionary<int, AudioCodec> audio_codec {
            get {
                if (_audio_codec != null)
                    return _audio_codec;
                else {
                    _audio_codec = new Dictionary<int, AudioCodec> {
                        { 0, new PCMU()},
                        { 8, new PCMA()},
                        { 9, new G722() }
                    };
                    return _audio_codec;
                }
            }
            set => _audio_codec = value;
        }

        /// <summary>
        /// Checks if we can support the specified media.
        /// </summary>
        /// <param name="media">SDP media.</param>
        /// <returns>Returns true if we can support this media, otherwise false.</returns>
        /// <exception cref="ArgumentNullException">Is raised when <b>media</b> is null reference.</exception>
        public bool CanSupportMedia(SDP_MediaDescription media) {
            if (media == null) {
                throw new ArgumentNullException("media");
            }

            if (!string.Equals(media.MediaType, SDP_MediaTypes.audio, StringComparison.InvariantCultureIgnoreCase)) {
                return false;
            }
            if (!string.Equals(media.Protocol, "RTP/AVP", StringComparison.InvariantCultureIgnoreCase)) {
                return false;
            }

            if (GetOurSupportedAudioCodecs(media).Count > 0) {
                return true;
            }

            return false;
        }

        /// <summary>
        /// Gets audio codecs which we can support from SDP media stream.
        /// </summary>
        /// <param name="media">SDP media stream.</param>
        /// <exception cref="ArgumentNullException">Is raised when <b>media</b> is null reference.</exception>
        /// <returns>Returns audio codecs which support.</returns>
        public Dictionary<int, AudioCodec> GetOurSupportedAudioCodecs(SDP_MediaDescription media) {
            if (media == null) {
                throw new ArgumentNullException("media");
            }

            var codecs = new Dictionary<int, AudioCodec>();

            // Check for IANA registered payload. Custom range is 96-127 and always must have rtpmap attribute.
            foreach (var format in media.MediaFormats) {
                var payload = Convert.ToInt32(format);
                if (payload < 96 && audio_codec.ContainsKey(payload)) {
                    if (!codecs.ContainsKey(payload)) {
                        codecs.Add(payload, audio_codec[payload]);
                    }
                }
            }
            // Check rtpmap payloads.
            foreach (var a in media.Attributes) {
                if (string.Equals(a.Name, "rtpmap", StringComparison.InvariantCultureIgnoreCase)) {
                    // Example: 0 PCMU/8000
                    var parts = a.Value.Split(' ');
                    var payload = Convert.ToInt32(parts[0]);
                    var codecName = parts[1].Split('/')[0];

                    foreach (var codec in audio_codec.Values) {
                        if (string.Equals(codec.Name, codecName, StringComparison.InvariantCultureIgnoreCase)) {
                            if (!codecs.ContainsKey(payload)) {
                                codecs.Add(payload, codec);
                            }
                        }
                    }
                }
            }

            return codecs;
        }

        /// <summary>
        /// Gets RTP stream mode.
        /// </summary>
        /// <param name="sdp">SDP message.</param>
        /// <param name="media">SDP media.</param>
        /// <returns>Returns RTP stream mode.</returns>
        /// <exception cref="ArgumentNullException">Is raised when <b>sdp</b> or <b>media</b> is null reference.</exception>
        public RTP_StreamMode GetRtpStreamMode(SDP_Message sdp, SDP_MediaDescription media) {
            if (sdp == null) {
                throw new ArgumentNullException("sdp");
            }
            if (media == null) {
                throw new ArgumentNullException("media");
            }

            // Try to get per media stream mode.
            foreach (var a in media.Attributes) {
                if (string.Equals(a.Name, "sendrecv", StringComparison.InvariantCultureIgnoreCase)) {
                    return RTP_StreamMode.SendReceive;
                } else if (string.Equals(a.Name, "sendonly", StringComparison.InvariantCultureIgnoreCase)) {
                    return RTP_StreamMode.Send;
                } else if (string.Equals(a.Name, "recvonly", StringComparison.InvariantCultureIgnoreCase)) {
                    return RTP_StreamMode.Receive;
                } else if (string.Equals(a.Name, "inactive", StringComparison.InvariantCultureIgnoreCase)) {
                    return RTP_StreamMode.Inactive;
                }
            }

            // No per media stream mode, try to get per session stream mode.
            foreach (var a in sdp.Attributes) {
                if (string.Equals(a.Name, "sendrecv", StringComparison.InvariantCultureIgnoreCase)) {
                    return RTP_StreamMode.SendReceive;
                } else if (string.Equals(a.Name, "sendonly", StringComparison.InvariantCultureIgnoreCase)) {
                    return RTP_StreamMode.Send;
                } else if (string.Equals(a.Name, "recvonly", StringComparison.InvariantCultureIgnoreCase)) {
                    return RTP_StreamMode.Receive;
                } else if (string.Equals(a.Name, "inactive", StringComparison.InvariantCultureIgnoreCase)) {
                    return RTP_StreamMode.Inactive;
                }
            }

            return RTP_StreamMode.SendReceive;
        }

        /// <summary>
        /// Gets SDP per media or global connection host.
        /// </summary>
        /// <param name="sdp">SDP message.</param>
        /// <param name="mediaStream">SDP media stream.</param>
        /// <returns>Returns SDP per media or global connection host.</returns>
        /// <exception cref="ArgumentNullException">Is raised when <b>sdp</b> or <b>mediaStream</b> is null reference.</exception>
        public string GetSdpHost(SDP_Message sdp, SDP_MediaDescription mediaStream) {
            if (sdp == null) {
                throw new ArgumentNullException("sdp");
            }
            if (mediaStream == null) {
                throw new ArgumentNullException("mediaStream");
            }

            // We must have SDP global or per media connection info.
            var host = mediaStream.Connection != null ? mediaStream.Connection.Address : null;
            if (host == null) {
                host = sdp.Connection.Address != null ? sdp.Connection.Address : null;

                if (host == null) {
                    throw new ArgumentException("Invalid SDP message, global or per media 'c'(Connection) attribute is missing.");
                }
            }

            return host;
        }

        /// <summary>
        /// Gets RTP target for SDP media stream.
        /// </summary>
        /// <param name="sdp">SDP message.</param>
        /// <param name="mediaStream">SDP media stream.</param>
        /// <returns>Return RTP target.</returns>
        /// <exception cref="ArgumentNullException">Is raised when <b>sdp</b> or <b>mediaStream</b> is null reference.</exception>
        public RTP_Address GetRtpTarget(SDP_Message sdp, SDP_MediaDescription mediaStream) {
            if (sdp == null) {
                throw new ArgumentNullException("sdp");
            }
            if (mediaStream == null) {
                throw new ArgumentNullException("mediaStream");
            }

            // We must have SDP global or per media connection info.
            var host = mediaStream.Connection != null ? mediaStream.Connection.Address : null;
            if (host == null) {
                host = sdp.Connection.Address != null ? sdp.Connection.Address : null;

                if (host == null) {
                    throw new ArgumentException("Invalid SDP message, global or per media 'c'(Connection) attribute is missing.");
                }
            }

            var remoteRtcpPort = mediaStream.Port + 1;
            // Use specified RTCP port, if specified.
            foreach (var attribute in mediaStream.Attributes) {
                if (string.Equals(attribute.Name, "rtcp", StringComparison.InvariantCultureIgnoreCase)) {
                    remoteRtcpPort = Convert.ToInt32(attribute.Value);

                    break;
                }
            }

            return new RTP_Address(System.Net.Dns.GetHostAddresses(host)[0], mediaStream.Port, remoteRtcpPort);
        }

        /// <summary>
        /// This method takes care of INVITE 2xx response retransmissions while ACK received.
        /// </summary>
        /// <param name="dialog">SIP dialog.</param>
        /// <param name="transaction">INVITE server transaction.</param>
        /// <exception cref="ArgumentException">Is raised when <b>dialog</b>,<b>transaction</b> is null reference.</exception>

        public void Handle2xx(SIP_Dialog dialog, SIP_ServerTransaction transaction) {
            if (dialog == null) {
                throw new ArgumentNullException("dialog");
            }
            if (transaction == null) {
                throw new ArgumentException("transaction");
            }

            /* RFC 6026 8.1.
				Once the response has been constructed, it is passed to the INVITE
				server transaction.  In order to ensure reliable end-to-end
				transport of the response, it is necessary to periodically pass
				the response directly to the transport until the ACK arrives.  The
				2xx response is passed to the transport with an interval that
				starts at T1 seconds and doubles for each retransmission until it
				reaches T2 seconds (T1 and T2 are defined in Section 17).
				Response retransmissions cease when an ACK request for the
				response is received.  This is independent of whatever transport
				protocols are used to send the response.
			 
				If the server retransmits the 2xx response for 64*T1 seconds without
				receiving an ACK, the dialog is confirmed, but the session SHOULD be
				terminated.  This is accomplished with a BYE, as described in Section
				15.
			  
				 T1 - 500
				 T2 - 4000
			*/

            TimerEx timer = null;

            EventHandler<SIP_RequestReceivedEventArgs> callback = delegate (object s1, SIP_RequestReceivedEventArgs e) {
                try {
                    if (e.Request.RequestLine.Method == SIP_Methods.ACK) {
                        // ACK for INVITE 2xx response received, stop retransmitting INVITE 2xx response.
                        if (transaction.Request.CSeq.SequenceNumber == e.Request.CSeq.SequenceNumber) {
                            if (timer != null) {
                                timer.Dispose();
                            }
                        }
                    }
                }
                catch {
                    // We don't care about errors here.
                }
            };
            dialog.RequestReceived += callback;

            // Create timer and sart retransmitting INVITE 2xx response.
            timer = new TimerEx(500) {
                AutoReset = false
            };
            timer.Elapsed += delegate (object s, System.Timers.ElapsedEventArgs e) {
                try {
                    lock (transaction.SyncRoot) {
                        if (transaction.State == SIP_TransactionState.Accpeted) {
                            transaction.SendResponse(transaction.FinalResponse);
                        } else {
                            timer.Dispose();

                            return;
                        }
                    }
                    timer.Interval = Math.Min(timer.Interval * 2, 4000);
                    timer.Enabled = true;
                }
                catch {
                    // We don't care about errors here.
                }
            };
            timer.Disposed += delegate (object s1, EventArgs e1) {
                try {
                    dialog.RequestReceived -= callback;
                }
                catch {
                    // We don't care about errors here.
                }
            };
            timer.Enabled = true;
        }


    }
}
